package org.spiderflow.core.expression.parsing.ast.call;

import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.ExpressionTemplateContext;
import org.spiderflow.core.expression.parsing.Span;
import org.spiderflow.core.expression.parsing.ast.Expression;
import org.spiderflow.core.expression.parsing.ast.access.VariableAccess;
import org.spiderflow.core.expression.reflection.JavaReflection;
import org.spiderflow.core.script.ScriptManager;

import javax.script.ScriptException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;

/**
 * Represents a call to a top-level function. A function may either be a {@link FunctionalInterface} stored in a
 * {@link ExpressionTemplateContext}, or a Macro defined in a template.
 *
 * @author zsh
 */
public class FunctionCall extends BaseCall {
    private final Expression expressionFunction;
    private Object cachedFunction;

    public FunctionCall(Span span, Expression expressionFunction, List<Expression> arguments) {
        super(span, arguments);
        this.expressionFunction = expressionFunction;
    }

    /**
     * Return the expression that must evaluate to a {@link FunctionalInterface} or a Macro.
     **/
    private Expression getFunction() {
        return expressionFunction;
    }


    /**
     * Returns the cached "function" descriptor as returned by {@link JavaReflection#getMethod(Object, String, Object...)} or the
     * Macro. See {@link #setCachedFunction(Object)}.
     **/
    private Object getCachedFunction() {
        return cachedFunction;
    }

    /**
     * Sets the "function" descriptor as returned by {@link JavaReflection#getMethod(Object, String, Object...)} for faster
     * lookups, or the Macro to be called. Called by {@link org.spiderflow.core.expression.interpreter.AstInterpreter} the first time this node is evaluated.
     * Subsequent evaluations can use the cached descriptor, avoiding a costly reflective lookup.
     **/
    private void setCachedFunction(Object cachedFunction) {
        this.cachedFunction = cachedFunction;
    }

    /**
     * This is a special case to handle template level macros. If a call to a macro is
     * made, evaluating the function expression will result in an exception, as the
     * function name can't be found in the context. Instead we need to manually check
     * if the function expression is a VariableAccess and if so,
     * if it can be found in the context.
     *
     * @param template 表达式模板
     * @param context  上下文
     * @return 执行结果
     * @throws IOException evaluate执行错误
     */
    @Override
    public Object evaluate(ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
        try {
            Object[] argumentValues = getArgumentValues(template, context);
            Object function = getFunction(template, context);
            if (function != null) {
                Object res = tryExecuteFunctionByCache(function, argumentValues);
                if (isSuccess(res)) {
                    return res;
                }
                return tryExecuteFunctionByReflection(argumentValues, function);
            } else if (isCustomScript()) {
                return tryExecuteCustomFunction(context, argumentValues);
            } else {
                throw ExpressionError.error("Couldn't find function " + getFunction(), getSpan());
            }
        } finally {
            clearCachedArguments();
        }
    }

    private Object getFunction(ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
        Object function;
        if (getFunction() instanceof VariableAccess) {
            VariableAccess varAccess = (VariableAccess) getFunction();
            function = context.get(varAccess.getVariableName().getText());
        } else {
            function = getFunction().evaluate(template, context);
        }
        return function;
    }

    private Object tryExecuteCustomFunction(ExpressionTemplateContext context, Object[] argumentValues) {
        try {
            return ScriptManager.eval(context, getFunction().getSpan().getText(), argumentValues);
        } catch (RuntimeException | ScriptException | NoSuchMethodException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }

    private boolean isCustomScript() {
        return ScriptManager.containsFunction(getFunction().getSpan().getText());
    }

    private Object tryExecuteFunctionByReflection(Object[] argumentValues, Object function) {
        Method method = JavaReflection.getInstance().getMethod(function, null, argumentValues);
        if (method == null) {
            throw ExpressionError.error("Couldn't find function.", getSpan());
        }
        setCachedFunction(method);
        try {
            return JavaReflection.getInstance().callMethod(function, method, argumentValues);
        } catch (RuntimeException t) {
            throw ExpressionError.error(t.getMessage(), getSpan(), t);
        }
    }

    private Object tryExecuteFunctionByCache(Object function, Object[] argumentValues) {
        Object method = getCachedFunction();
        if (method != null) {
            try {
                return JavaReflection.getInstance().callMethod(function, method, argumentValues);
            } catch (RuntimeException t) {
                // fall through
            }
        }
        return FAIL_FLAG;
    }
}
